﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics;

namespace IsmsExplorer.Infrastructure.Helper {
    /// <summary>
    /// Utility class to facilitate handling of ISO 27001:2005 Control references.
    /// A valid reference is of the form: "A.x.y.z" or "A x.y.z" (where x,y,z is a one or two digit integer).
    /// </summary>
    public static class Iso27001Helper {
        private static readonly int _char0Val = (int)'0';
        // This will match exactly one reference ID - the string must be just the reference ID and only space characters other than
        // the reference ID are tolerated, otherwise it does not match!
        private static Regex _refIdRegexExact = new Regex(@"^\s*A(\.|\s)(?<section>[0-9]{1,2})\.(?<subsection>[0-9]{1,2})\.(?<number>[0-9]{1,2})\s*$", RegexOptions.Compiled);
        // This will match one or more reference IDs in any text.
        private static Regex _refIdRegexMatchAny = new Regex(@"A(\.|\s)(?<section>[0-9]{1,2})\.(?<subsection>[0-9]{1,2})\.(?<number>[0-9]{1,2})", RegexOptions.Compiled);
        
        public static MatchCollection Matches(string strInput) {
            return _refIdRegexMatchAny.Matches(strInput);
        }

        public static MatchCollection Matches(string strInput, int startAt) {
            return _refIdRegexMatchAny.Matches(strInput, startAt);
        }

        public static bool Validate(string refID) {
            if (!string.IsNullOrEmpty(refID)) {
                return _refIdRegexExact.IsMatch(refID);
            }
            return false;
        }

        /// <summary>
        /// Given a string containing an ISO Control reference ID of the form A.x.y.z (where x,y,z is a one or two digit integer) it
        /// parses it and returns the 3 different sections as strings, corresponding to x, y, z.
        /// </summary>
        /// <param name="refID"></param>
        /// <param name="x"></param>
        /// <param name="y"></param>
        /// <param name="z"></param>
        /// <returns>true if parsing is successful, false if not</returns>
        public static bool GetSections(string refID, out string x, out string y, out string z) {
            x = null;
            y = null;
            z = null;
            Match m = _refIdRegexExact.Match(refID);
            if (m.Success) {
                x = m.Groups[2].Value;
                y = m.Groups[3].Value;
                z = m.Groups[4].Value;
                return true;
            } else {
                return false;
            }
        }

        /// <summary>
        /// Compares two reference IDs by sections. This is important when sorting reference IDs because
        /// in a typical string sorting, if the sections are not taken into account, A.10.1.2 comes before A.5.1.2 
        /// which is incorrect according to how we want them sorted (5 is less than 10).
        /// </summary>
        /// <param name="refID1"></param>
        /// <param name="?"></param>
        /// <returns></returns>
        public static int CompareSectionsRegex(string refID1, string refID2) {
            Match m1 = _refIdRegexExact.Match(refID1);
            Match m2 = _refIdRegexExact.Match(refID2);
            Debug.Assert(m1.Success && m2.Success, "Invalid string input (not valid Reference IDs)");
            // May want to comment the following test out if assuming valid control refIDs, to speed up comparisons.
            if (!m1.Success || !m2.Success) {
                throw new ArgumentException("Invalid string input (not valid Reference IDs)");
            }
            int c = Convert.ToInt32(m1.Groups[2].Value) - Convert.ToInt32(m2.Groups[2].Value);
            if (c == 0) {
                c = Convert.ToInt32(m1.Groups[3].Value) - Convert.ToInt32(m2.Groups[3].Value);
                if (c == 0) {
                    c = Convert.ToInt32(m1.Groups[4].Value) - Convert.ToInt32(m2.Groups[4].Value);
                }
            }
            return Math.Sign(c);
        }

        /// <summary>
        /// NOTE: we are assuming that the two strings are <b>valid</b> control reference IDs of the form: 
        ///       A.x.y.z (where x,y,z is a one or two digit integer)
        /// </summary>
        /// <param name="refID1"></param>
        /// <param name="refID2"></param>
        /// <returns></returns>
        public static int CompareSections(string refID1, string refID2) {
            Debug.Assert(!string.IsNullOrEmpty(refID1));
            Debug.Assert(!string.IsNullOrEmpty(refID2));
            Debug.Assert(refID1.StartsWith("A."));
            Debug.Assert(refID2.StartsWith("A."));
            // Start from the first digit (assuming ref IDs are valid and start by "A.".
            int i1 = 2;
            int i2 = 2;
            int c = Math.Sign(ExtractInt(refID1, ref i1) - ExtractInt(refID2, ref i2));
            if (c == 0) {
                // Go past the '.'
                ++i1; ++i2;
                c = Math.Sign(ExtractInt(refID1, ref i1) - ExtractInt(refID2, ref i2));
                if (c == 0) {
                    // Go past the '.'
                    ++i1; ++i2;
                    c = Math.Sign(ExtractInt(refID1, ref i1) - ExtractInt(refID2, ref i2));
                }
            }
            return Math.Sign(c);
        }

        private static int ExtractInt(string s, ref int startAt) {
            int val = 0;
            while ((startAt < s.Length) && Char.IsDigit(s[startAt])) {
                val = val * 10 + (((int)s[startAt]) - _char0Val);
                ++startAt;
            }
            // val cannot be 0 because ISO Control RefIDs do not have the zero digit.
            Debug.Assert(val > 0);
            return val;
        }

    } // end public static class Iso27001Helper

}
